本文将介绍如何把 Redux
中优秀的设计模式和 TypeScript
这门语言结合起来,全文会围绕 Redux store
设计、Redux reducer
设计、Redux action
的设计以及常用异步中间件(redux-thunk
和 redux-promise-middleware
)的使用展开。
接着实践二讲,本文要实现的功能是,在页面加载完成之前,通过 Redux
请求 /user/detail
接口获取用户基本信息并把信息写入到对应的 store
中。
页面组件 User
的代码如下:
|
|
Redux store
设计
基于 Ducks
架构,我们会根据每个页面状态划分出一个独立的 state
,将这些页面的 state
组合在一起形成一个完整的 store
状态树。以 User
页面为例,根据上述 User
页面组件的代码可以看出 User
组件依赖以下四个属性:
name | type | description |
---|---|---|
loading | boolean | 页面是否处于加载 |
name | string | 用户姓名 |
age | string | 用户年龄 |
department | string | 用户所属部门 |
其中 loading
属于页面 UI
状态,name
、age
和 department
属于请求 /user/detail
接口获取到的一部分数据,我们可以简单地把没有业务属性的 UI
状态划分为一类数据,其他数据再按照各自的业务属性划分成不同类:
|
|
Redux action
设计
先看 Redux
本身对于 action
的接口定义:
|
|
Redux action
的原始接口定义很简单,必须要有一个任意类型的 type
属性,而其他属性可以让用户随意定制,在实际应用中,这种接口定义不够严格和规范,会出现各式各样的 action
,因此,我们将采用 Flux action
标准来规范 Redux action
的使用。
Flux action
的属性有:
name | type | required | description |
---|---|---|---|
type | string | √ | action 标识 |
payload | any | action 数据载荷 |
|
error | any | 标志 action 是否是异常的,如果为真,则 payload 应该为 Error 对象 |
|
meta | any | action 中不属于 payload 的额外数据信息 |
根据 Flux action
的属性可以定义 FluxAction
接口:
|
|
在 Redux
中,action
会由 action creator
函数生成,Redux
对于 action creator
的原始接口定义为:
|
|
基于 ActionCreator
,我们可以构造出专门生成 FluxAction
类型 action
的 action creator
:
|
|
假设我们现在需要切换 User
组件上的 loading
状态,要求通过 action creator
发出对应的 FluxAction
类型的 action
,那么我们可以这么定义这个 action creator
:
|
|
当然,FluxActionCreator
类型定义稍显复杂,而复杂的类型定义会带来一定的理解成本,因此,我们也可以将 toggleLoading
简单定义为:
|
|
再比如修改 IUserState.user
:
|
|
Redux
异步action
设计
Redux
本身不支持异步 action
,需要引入额外的中间件来支持,这里举两个常见的异步中间件 redux-thunk
和 redux-promise-middleware
,我们将分别讲解如何通过这两个中间件从 /user/detail
接口获取用户基本信息。
I. redux-thunk
通常情况下,action creator
函数会返回一个对象,而在引入 redux-thunk
中间件后,action creator
会返回一个函数,该函数参数如下表:
name | type | description |
---|---|---|
dispatch | ThunkDispatch | action creator 触发函数 |
getState | function(): any | 获取整个 store tree 函数 |
extraArgument | any | 额外的参数 |
|
|
基于以上源码,我们可以构造出 FluxAction
相关的 ThunkDispatch
和 ThunkAction
|
|
在 FluxThunkDispatch
和 FluxThunkAction
中,我们调整了泛型类型的定义顺序,并对 M
、R
、E
提供了默认的类型 any
,这是因为在大多数情况下,我们更关心代表 store tree
类型的 S
和代表 action payload
类型的 P
。来看个使用 FluxThunkAction
的例子:
|
|
通常情况下,一个应用只会配一个 store tree
,因此 FluxThunkAction
和 FluxThunkDispatch
可以简化为:
|
|
上述例子就可以简化为:
|
|
现在再看从 /user/detail
接口获取用户信息功能,要求服务端在响应过程中,前端要展示 loading
状态,等到服务端返回后前端再取消 loading
状态,通过 redux-thunk
可以这么实现 action creator
:
|
|
II. redux-promise-middleware
redux-thunk
在 action creator
内部处理不同的异步状态,而 redux-promise-middleware
则把这些处理逻辑移至 reducer
中处理,action creator
应该尽量保持代码简洁,减少衍生计算逻辑,从这点看,redux-promise-middleware
要优于 redux-thunk
并且 redux-promise-middleware
对于 Promise
异步状态划分的更为清晰,代码可读性更强。
redux-promise-middleware
要求 action payload
类型为 Promise
类型或是包含以下两个属性的对象
name | type | description |
---|---|---|
promise | Promise | Promise 对象 |
data | any | 乐观更新的数据 |
|
|
从 /user/detail
接口获取用户信息的 action creator
接口可以写成:
|
|
相比于 redux-thunk
,redux-promise-middleware
的 action creator
代码要简洁很多。
Redux reducer
设计
设计好 Redux store
后,还需要定义对应的 Redux reducer
来接收 Redux action
并对 Redux store
进行衍生计算。
先看 Redux
对 Reducer
类型的定义:
|
|
结合 FluxAction
和 IStoreTree
可以定义我们自己想要的 Reducer
和 ReducersMapObject
:
|
|
当涉及到组件状态时候,不可变数据总是绕不开的话题,社区里常用的不可变数据函数库有 immutable
和 immer
等等,由于 immutable
对于数据结构处理较为繁琐,需要在它提供的对象和原始 javascript
对象间来回切换,故打算采用更为轻量的 immer
库,为了让开发者对不可变数据无感知,需要对每个 reducer
做些额外处理:
|
|
构建好不可变状态后,可以开始实现 user reducer
的功能:
|
|
上述代码中 reducer
函数是标准的 Redux reducer
,这种方式存在函数过长、功能划分不够细致、 action payload
类型不确定等弊端,可以尝试使用 type-to-reducer
库对其进行简化:
|
|
至此还剩最后三个步骤:
把不可变
reducer
的创建和普通reducer
结合起来12345678// 文件: client/store/root-reducer.ts// reducer 集合import createImReducers from './create-immutable-reducers'import user from '../pages/user/user'export default createImReducers({user})创建
store
并引入中间件1234567891011121314// 文件: client/store/create-store.tsimport Thunk from 'redux-thunk'import promiseMiddleware from 'redux-promise-middleware'import { createStore, combineReducers, applyMiddleware } from 'redux'import reducers from './root-reducer'export default function(initialState:IStoreTree):any {return createStore(combineReducers(reducers),initialState,applyMiddleware(promiseMiddleware(), Thunk))}User
页面组件连接redux
1234567891011121314151617181920212223242526272829303132333435363738394041// 文件: client/pages/user/index.tsximport {connect} from 'react-redux'import {bindActionCreators, Dispatch} from 'redux'import {Spin, Input} from 'antd'import {getUserDetail} from './user'// User 组件属性接口interface IProps extends IUserState {actions: any}// User 组件class User extends React.Component<IProps> {componentDidMount() {this.props.actions.getUserDetail('xxxx')}render() {const {ui, user} = this.propsreturn (<Spin spinning={ui.loading}><strong>用户姓名:</strong><Input value={user.name} /><strong>用户年龄:</strong><Input value={user.age} /><strong>所属部门:</strong><Input value={user.department} /></Spin>)}}export default connect((store: IStoreTree): IUserState => store.user,(dispatch: Dispatch<FluxAction>) => ({actions: bindActionCreators({getUserDetail}, dispatch)}))